/*
 * scullpriv.c  create one cloned private device
 */

#include <linux/module.h>
#include <linux/init.h>

#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/cdev.h>
#include <linux/tty.h>
#include <linux/list.h>

#include <asm/uaccess.h>
#include <asm/atomic.h>

#include "scullpriv.h"

static int scullpriv_major = 0;
static dev_t devno;
static struct scullpriv_dev *scullpriv_device;
static spinlock_t scullpriv_lock = SPIN_LOCK_UNLOCKED; /* static */

static DECLARE_WAIT_QUEUE_HEAD(scullpriv_wait);

struct scull_listitem {
	struct scullpriv_dev device;
	dev_t key;
	struct list_head list;
    
};

static LIST_HEAD(scullpriv_list);

int scull_trim(struct scullpriv_dev *dev)
{
	struct scull_qset *next, *dptr;
	int qset = dev->qset;   /* "dev" is not-null */
	int i;

	for (dptr = dev->data; dptr; dptr = next) { /* all the list items */
		if (dptr->data) {
			for (i = 0; i < qset; i++)
				kfree(dptr->data[i]);
			kfree(dptr->data);
			dptr->data = NULL;
		}
		next = dptr->next;
		kfree(dptr);
	}
	dev->size = 0;
	dev->quantum = 4000;
	dev->qset = 1000;
	dev->data = NULL;
	return 0;
}

struct scull_qset *scull_follow(struct scullpriv_dev *dev, int n)
{
	struct scull_qset *qs = dev->data;

        /* Allocate first qset explicitly if need be */
	if (! qs) {
		qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
		if (qs == NULL)
			return NULL;  /* Never mind */
		memset(qs, 0, sizeof(struct scull_qset));
	}

	/* Then follow the list */
	while (n--) {
		if (!qs->next) {
			qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
			if (qs->next == NULL)
				return NULL;  /* Never mind */
			memset(qs->next, 0, sizeof(struct scull_qset));
		}
		qs = qs->next;
		continue;
	}
	return qs;
}

static struct scullpriv_dev *scullpriv_lookfor_device(dev_t key)
{
	struct scull_listitem *lptr;

	list_for_each_entry(lptr, &scullpriv_list, list) {
		if (lptr->key == key)
			return &(lptr->device);
	}

	/* not found */
	lptr = kmalloc(sizeof(struct scull_listitem), GFP_KERNEL);
	if (!lptr)
		return NULL;

	/* initialize the device */
	memset(lptr, 0, sizeof(struct scull_listitem));
	lptr->key = key;
	scull_trim(&(lptr->device)); /* initialize it */
	init_MUTEX(&(lptr->device.sem));

	/* place it in the list */
	list_add(&lptr->list, &scullpriv_list);

	return &(lptr->device);
}

static int scullpriv_open(struct inode *inode, struct file *filp)
{
	struct scullpriv_dev *dev;
	dev_t key;
 
	if (!current->signal->tty) { 
		WXYDEBUG("Process \"%s\" has no ctl tty\n", current->comm);
		return -EINVAL;
	}
	key = tty_devnum(current->signal->tty);

	/* look for a scullc device in the list */
	spin_lock(&scullpriv_lock);
	dev = scullpriv_lookfor_device(key);
	spin_unlock(&scullpriv_lock);

	if (!dev)
		return -ENOMEM;

	/* then, everything else is copied from the bare scull device */
	if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
		scull_trim(dev);
	filp->private_data = dev;

	return 0;          /* success */
}

ssize_t scullpriv_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
	struct scullpriv_dev *dev = filp->private_data; 
	struct scull_qset *dptr;	/* the first listitem */
	int quantum = dev->quantum, qset = dev->qset;
	int itemsize = quantum * qset; /* how many bytes in the listitem */
	int item, s_pos, q_pos, rest;
	ssize_t retval = 0;

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;
	if (*f_pos >= dev->size)
		goto out;
	if (*f_pos + count > dev->size)
		count = dev->size - *f_pos;

	/* find listitem, qset index, and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;

	/* follow the list up to the right position (defined elsewhere) */
	dptr = scull_follow(dev, item);

	if (dptr == NULL || !dptr->data || ! dptr->data[s_pos])
		goto out; /* don't fill holes */

	/* read only up to the end of this quantum */
	if (count > quantum - q_pos)
		count = quantum - q_pos;

	if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;
	retval = count;

  out:
	up(&dev->sem);
	return retval;
}

ssize_t scullpriv_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	struct scullpriv_dev *dev = filp->private_data;
	struct scull_qset *dptr;
	int quantum = dev->quantum, qset = dev->qset;
	int itemsize = quantum * qset;
	int item, s_pos, q_pos, rest;
	ssize_t retval = -ENOMEM; /* value used in "goto out" statements */

	if (down_interruptible(&dev->sem))
		return -ERESTARTSYS;

	/* find listitem, qset index and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum; q_pos = rest % quantum;

	/* follow the list up to the right position */
	dptr = scull_follow(dev, item);
	if (dptr == NULL)
		goto out;
	if (!dptr->data) {
		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
		if (!dptr->data)
			goto out;
		memset(dptr->data, 0, qset * sizeof(char *));
	}
	if (!dptr->data[s_pos]) {
		dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
		if (!dptr->data[s_pos])
			goto out;
	}
	/* write only up to the end of this quantum */
	if (count > quantum - q_pos)
		count = quantum - q_pos;

	if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
		retval = -EFAULT;
		goto out;
	}
	*f_pos += count;
	retval = count;

        /* update the size */
	if (dev->size < *f_pos)
		dev->size = *f_pos;

  out:
	up(&dev->sem);
	return retval;
}

static int scullpriv_release(struct inode *inode, struct file *filp)
{
	return 0;
}


struct file_operations scullpriv_fops = {
	.owner = THIS_MODULE,
	.open = scullpriv_open,
	.read = scullpriv_read,
	.write = scullpriv_write,
	.release = scullpriv_release
}; 

static int scullpriv_init(void)
{
	int result, err;

	if (scullpriv_major) {
                devno = MKDEV(scullpriv_major, 0);
                result = register_chrdev_region(devno, 1, "scullpriv");
        } else {
                result = alloc_chrdev_region(&devno, 0, 1, "scullpriv");
                scullpriv_major = MAJOR(devno);
        }
        if (result < 0) {
                printk(KERN_WARNING "scullpriv: can't get major %d\n", scullpriv_major);
                return result;
        }

	scullpriv_device = kmalloc(sizeof(struct scullpriv_dev), GFP_KERNEL);
	if (!scullpriv_device) /* if (scullpriv_device == NULL) */ {
                unregister_chrdev_region(devno, 1);
                return -ENOMEM;
        }

        memset(scullpriv_device, 0, sizeof(struct scullpriv_dev));

	scullpriv_device->quantum = 4000;
	scullpriv_device->qset = 1000;
	init_MUTEX(&scullpriv_device->sem);

	cdev_init(&scullpriv_device->cdev, &scullpriv_fops);
	scullpriv_device->cdev.owner = THIS_MODULE;
	err = cdev_add(&scullpriv_device->cdev, devno, 1);
	if (err) {
		printk(KERN_NOTICE "Error %d adding scullpriv_device.\n", err);
		kobject_put(&scullpriv_device->cdev.kobj);
		goto fail;
	} else
		printk(KERN_NOTICE "scullpriv_device registered at %x\n", devno);
	
	return 0;
fail:
	kfree(scullpriv_device);
	unregister_chrdev_region(devno, 1);
	return err;
}

static void scullpriv_cleanup(void)
{
	cdev_del(&scullpriv_device->cdev);
	scull_trim(scullpriv_device);
	kfree(scullpriv_device);
	unregister_chrdev_region(devno, 1);
}

module_init(scullpriv_init);
module_exit(scullpriv_cleanup);

MODULE_AUTHOR("WXY");
MODULE_LICENSE("GPL");
